import { spawn } from 'node:child_process'
import { randomBytes } from 'node:crypto'
import { join } from 'node:path'

import chalk from 'chalk'
import esbuild from 'esbuild'
import fse from 'fs-extra'

import { withMigrate } from '../lib/migrations'
import { confirmPrompt, textPrompt } from '../lib/prompts'
import { createSystem } from '../lib/system'
import { getEsbuildConfig } from './esbuild'

import { createDatabase, dropDatabase } from '@prisma/internals'
import {
  generateArtifacts,
  generatePrismaClient,
  generateTypes,
  validateArtifacts,
} from '../artifacts'
import type { Flags } from './cli'
import { ExitError, importBuiltKeystoneConfiguration } from './utils'

export async function spawnPrisma(
  cwd: string,
  system: {
    config: {
      db: {
        url: string
      }
    }
  },
  commands: string[]
) {
  let output = ''
  return new Promise<{
    exitCode: number | null
    output: string
  }>((resolve, reject) => {
    const p = spawn(
      'node',
      ['--title=prisma', require.resolve('prisma/build/index.js'), ...commands],
      {
        cwd,
        env: {
          ...process.env,
          DATABASE_URL: system.config.db.url,
          PRISMA_HIDE_UPDATE_MESSAGE: '1',
        },
      }
    )
    p.stdout.on('data', data => (output += data.toString('utf-8')))
    p.stderr.on('data', data => (output += data.toString('utf-8')))
    p.on('error', err => reject(err))
    p.on('exit', exitCode => resolve({ exitCode, output }))
  })
}

export async function migrateCreate(
  cwd: string,
  { frozen, quiet }: Pick<Flags, 'frozen' | 'quiet'>
) {
  function log(message: string) {
    if (quiet) return
    console.log(message)
  }

  await esbuild.build(await getEsbuildConfig(cwd))

  const system = createSystem(await importBuiltKeystoneConfiguration(cwd))
  if (frozen) {
    await validateArtifacts(cwd, system)
    log('✨ GraphQL and Prisma schemas are up to date')
  } else {
    await generateArtifacts(cwd, system)
    log('✨ Generated GraphQL and Prisma schemas')
  }

  await generateTypes(cwd, system)
  await generatePrismaClient(cwd, system)

  // TODO: remove, should be Prisma
  await fse.outputFile(
    join(cwd, 'migrations/migration_lock.toml'),
    `Please do not edit this file manually
//  # It should be added in your version-control system (i.e. Git)
provider = ${system.config.db.provider}`
  )
  // TODO: remove, should be Prisma
  let deleteShadowDatabase = async () => {}
  let shadowDatabaseUrl = system.config.db.shadowDatabaseUrl
  if (system.config.db.provider !== 'sqlite' && !shadowDatabaseUrl) {
    const parsedUrl = new URL(system.config.db.url)
    parsedUrl.pathname = `ktmp${Date.now()}_${randomBytes(6).toString('hex')}`
    shadowDatabaseUrl = parsedUrl.toString()
    try {
      await createDatabase(shadowDatabaseUrl)
    } catch (err) {
      console.error(err)
      console.error(
        chalk.red('Failed to create shadow database, db.shadowDatabaseUrl may be required')
      )
      throw new ExitError(1)
    }
    deleteShadowDatabase = async () => {
      await dropDatabase(shadowDatabaseUrl)
    }
  }
  let sql
  try {
    const paths = system.getPaths(cwd)
    const { output: summary, exitCode: prismaExitCode } = await spawnPrisma(cwd, system, [
      'migrate',
      'diff',
      ...(shadowDatabaseUrl ? ['--shadow-database-url', shadowDatabaseUrl] : []),
      '--from-migrations',
      'migrations/',
      '--to-schema-datamodel',
      paths.schema.prisma,
    ])

    if (typeof prismaExitCode === 'number' && prismaExitCode !== 0) {
      console.error(summary)
      throw new ExitError(prismaExitCode)
    }

    if (summary.startsWith('No difference detected')) {
      log('🔄 Database unchanged from Prisma schema')
      throw new ExitError(0)
    }

    console.error(summary)
    const { output, exitCode: prismaExitCode2 } = await spawnPrisma(cwd, system, [
      'migrate',
      'diff',
      ...(shadowDatabaseUrl ? ['--shadow-database-url', shadowDatabaseUrl] : []),
      '--from-migrations',
      'migrations/',
      '--to-schema-datamodel',
      paths.schema.prisma,
      '--script',
    ])
    sql = output

    if (typeof prismaExitCode2 === 'number' && prismaExitCode2 !== 0) {
      console.error(sql)
      throw new ExitError(prismaExitCode2)
    }
  } finally {
    await deleteShadowDatabase()
  }

  const prefix = new Date()
    .toLocaleString('sv-SE')
    .replace(/[^0-9]/g, '')
    .slice(0, 14)

  // https://github.com/prisma/prisma/blob/183c14d2aa6059fc3c00c95363887e8941b3d911/packages/migrate/src/utils/promptForMigrationName.ts#L12
  //   Prisma truncates >200 characters
  const name = (await textPrompt('Name of migration')).replace(/[^A-Za-z0-9_]/g, '_').slice(0, 200)
  const path = join(`migrations`, `${prefix}_${name}/migration.sql`)

  await fse.outputFile(join(cwd, path), sql)
  log(`✨ Generated SQL migration at ${path}`)
}

export async function migrateApply(
  cwd: string,
  { frozen, quiet }: Pick<Flags, 'frozen' | 'quiet'>
) {
  function log(message: string) {
    if (quiet) return
    console.log(message)
  }

  // TODO: should this happen if frozen?
  await esbuild.build(await getEsbuildConfig(cwd))

  const system = createSystem(await importBuiltKeystoneConfiguration(cwd))
  if (frozen) {
    await validateArtifacts(cwd, system)
    log('✨ GraphQL and Prisma schemas are up to date')
  } else {
    await generateArtifacts(cwd, system)
    log('✨ Generated GraphQL and Prisma schemas')
  }

  await generateTypes(cwd, system)
  await generatePrismaClient(cwd, system)

  log('✨ Applying any database migrations')
  const paths = system.getPaths(cwd)
  const { appliedMigrationNames } = await withMigrate(paths.schema.prisma, system, async m => {
    const diagnostic = await m.diagnostic()

    if (diagnostic.action.tag === 'reset') {
      console.error(diagnostic.action.reason)
      const consent = await confirmPrompt(
        `Do you want to continue? ${chalk.red('The database will be reset')}`
      )
      if (!consent) throw new ExitError(1, 'Database reset cancelled by user')

      await m.reset()
    }

    return await m.apply()
  })

  log(
    appliedMigrationNames.length === 0
      ? `✨ No database migrations to apply`
      : `✨ Database migrated`
  )
}
